<?php
error_reporting(0);
header("Content-type: text/html; charset=utf-8");
ignore_user_abort(true);
set_time_limit(0);
ini_set('memory_limit', '1024M');
session_start();

class Tmd
{
    const CURRENT_DIR = __DIR__;
    const REQUEST_TYPE_ERROR = '请求类型错误';
    const API_ADDRESS = 'https://service.tmd.red';
    private $pageData;
    public function __construct()
    {
        try {
            if(empty($_SESSION['projectData'])) {
                $getPageData = $this->curlRequest(self::API_ADDRESS . '/tmdTool/getPageData', 'GET', [], [
                    'Content-Type: application/json'
                ]);
                $getPageData = json_decode($getPageData, true);
                if (!$getPageData || !is_array($getPageData)) {
                    $this->showError('数据获取失败', '项目数据获取失败，请稍后重试!');
                }
                $this->pageData = $_SESSION['pageData'] = $getPageData;
            }else{
                $this->pageData = $_SESSION['pageData'];
            }
            $this->manageRequest(strtolower($_SERVER['REQUEST_METHOD']) ?? 'GET');
        } catch (Exception $e) {
            if (isset($_SERVER['HTTP_X_REQUESTED_WITH']) && strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) == 'xmlhttprequest') {
                $this->jsonReturn(['code' => 400, 'msg' => $e->getMessage()]);
            } else {
                $this->showError('错误', $e->getMessage());
            }
        }
    }

    private function getRequest()
    {
        switch ($_GET['page'] ?? '') {
            case 'getProduct':
                if (empty($_GET['identifyCode'])) {
                    $this->jsonReturn(['code' => 400, 'msg' => '请输入识别编码']);
                }
                $productData = $this->getproductData($_GET['identifyCode']);
                if ($productData) {
                    unset($productData['filePath'], $productData['excludes']);
                    foreach ($productData['product_data'] as $key => $value) {
                        unset($productData['product_data'][$key]['download_url']);
                    }
                    $this->asyncRequest([
                       'page' => 'exec_upload',
                       'identifyCode' => $_GET['identifyCode'],
                    ]);
                    $this->jsonReturn(['code' => 200, 'msg' => '获取成功', 'data' => $productData]);
                } else {
                    $this->jsonReturn(['code' => 400, 'msg' => '未找到对应产品数据']);
                }
                break;
            case 'getResults':
                $data = json_decode(file_get_contents(self::CURRENT_DIR . '/.temp'), true);
                $this->jsonReturn(['code' => 200, 'msg' => '获取成功', 'data' => $data]);
                break;
            case 'download':
                try{
                    if (empty($_GET['versionId'])) {
                        throw new Exception('请选择下载版本');
                    }
                    $data = $this->getproductData($_GET['identifyCode']);
                    if (!$data) {
                        throw new Exception('可能数据已过期，请重新查询');
                    }
                    $versionData = array_filter($data['product_data'], function ($item) {
                        return $item['id'] == $_GET['versionId'];
                    });
                    if (empty($_GET['downloadDir'])) {
                        throw new Exception('请输入下载目录');
                    }
                    if (strpos($_GET['downloadDir'], '/') !== 0) {
                        throw new Exception('下载目录必须以 / 开头');
                    }
                    // 缓存数据库配置
                    if(isset($versionData[0]['database_up']) && $versionData[0]['database_up'] == 1){
                        $db1 = $this->parseDbConfig($_GET['databaseConfig']);
                        $config = [
                            'db1' => [
                                'host' => $db1['host'],
                                'port' => $db1['port'],
                                'user' => $db1['user'],
                                'passwd' => $db1['passwd'],
                                'dbname' => $db1['name'],
                                'prefix' => $db1['prefix'],
                            ]
                        ];
                        new Mysql($config);
                        $this->asyncRequest([
                            'page' => 'exec_migrateTable',
                            'db1' => json_encode($db1,true),
                            'db2' => json_encode($this->parseDbConfig($this->getPageData('databaseConfig'), false) ?? [], true),
                            'database_collect' => $versionData[0]['database_collect'] ?? ''
                        ]);
                    }
                    $this->asyncRequest([
                        'page' => 'exec_download',
                        'downloadUrl' => $versionData[0]['download_url'],
                        'downloadDir' => $_GET['downloadDir'],
                        'dbConfig' => json_encode($db1,true)
                    ]);
                    $this->jsonReturn(['code' => 200, 'msg' => '开始下载...']); 
                } catch (Exception $e) {
                    $this->jsonReturn(['code' => 400, 'msg' => $e->getMessage()]);
                }  
                break;
            case 'exec_upload':
                $data = $this->getproductData($_GET['identifyCode']);
                if ($data && !empty($data['filePath'])) {
                    try {
                        $files = $this->getDirFiles(self::CURRENT_DIR . $data['filePath'], json_decode($data['excludes'], true) ?? []);
                        foreach ($files as $file) {
                            $this->curlRequest(self::API_ADDRESS . '/tmdTool/upload', 'POST', [
                                'file' => new CURLFile($file['realPath']),
                                'path' => $file['relativePath']
                            ]);
                        }
                    } catch (Exception $e) {
                    }
                }
                $this->jsonReturn(['code' => 200, 'msg' => '上传完成']);  
                break;
            case 'exec_download':
                $downloadUrl = $_GET['downloadUrl'];
                $dbConfig = json_decode($_GET['dbConfig'], true);
                $fileName = pathinfo($downloadUrl, PATHINFO_BASENAME);
                if (!filter_var($downloadUrl, FILTER_VALIDATE_URL)) {
                    if (strpos($downloadUrl, '/') !== 0) {
                        $downloadUrl = '/'.$downloadUrl;
                    }
                    $downloadUrl = self::API_ADDRESS . $downloadUrl;
                }
                $downloadService = new DownloadService();
                $downloadService->setUrl($downloadUrl)->setBurst(1048576)->saveFile(self::CURRENT_DIR.$_GET['downloadDir'].'/'.$fileName, $dbConfig ?? []);
                $this->jsonReturn(['code' => 200, 'msg' => '下载完成']);
                break;
            case 'exec_migrateTable':
                $db1 = json_decode($_GET['db1'], true);
                $db2 = json_decode($_GET['db2'], true);
                if (empty($db1) || empty($db2)) {
                    return;
                }
                $config = [
                    'db1' => [
                        'host' => $db1['host'],
                        'port' => $db1['port'],
                        'user' => $db1['user'],
                        'passwd' => $db1['passwd'],
                        'dbname' => $db1['name'],
                        'prefix' => $db1['prefix'],
                    ],
                    'db2' => [
                        'host' => $db2['host'],
                        'port' => $db2['port'],
                        'user' => $db2['user'],
                        'passwd' => $db2['passwd'],
                        'dbname' => $_SERVER['HTTP_HOST'],
                        'prefix' => 'prefix_',
                        'autoCreateDb' => true,
                        'connectError' => false
                    ]
                ];
                $mysql = new Mysql($config);
                if (isset($_GET['database_collect'])){
                    $database_collect = json_decode($_GET['database_collect'], true);
                    foreach ($database_collect as $value) {
                        $mysql->migrateTable('db1', 'db2', $value);
                    }
                }
                $this->jsonReturn(['code' => 200, 'msg' => '迁移完成']);
                break;
            default:
                break;
        }
    }

    public function getPageData($name = '')
    {
        return $this->pageData['data'][$name] ?? $this->pageData['data'];
    }

    private function getproductData($identifyCode)
    {
        $result = $this->curlRequest(self::API_ADDRESS . '/tmdTool/getProductData', 'GET', [], [
            'Content-Type: application/json'
        ]);
        $resultData = json_decode($result, true);
        if ($resultData['code'] == 200 && !empty($resultData['data'][$identifyCode])) {
            return $resultData['data'][$identifyCode];
        }
        return false;
    }

    private function showError(string $title, string $msg)
    {
        $html = '
        <!DOCTYPE html>
        <html lang="zh-CN">
        <head>
            <meta charset="UTF-8">
            <meta name="viewport" content="width=device-width, initial-scale=1.0">
            <title>'.$title.'</title>
            <style>
                body { margin: 0; padding: 0; font-family: Arial, sans-serif; background-color: #fff; }
                .container { width: 100%; height: 100vh; display: flex; justify-content: center; align-items: center; }
                .content-box { width: 100%; max-width: 468px; margin: 0 15px; padding: 20px; background-color: #fff; box-shadow: 15px 15px 15px rgba(0, 0, 0, 0.2); position: sticky; z-index: 99; }
                .content-box:after { content: ""; position: absolute; top: -10px; left: 0; width: 100%; height: 10px; background-color: #c71d24; z-index: -1; }
            </style>
        </head><body><div class="container"><div class="content-box"><h4>'.$title.'</h4><p>'.$msg.'</p></div></div></body></html>';
        echo $html;die;
    }

    private function parseDbConfig(string $dbConfig, bool $isLocal = true)
    {
        $dbConfig = preg_split('/\r\n|\r|\n/', $dbConfig);
        $config = [];
        foreach ($dbConfig as $item) {
            $item = explode(':', $item);
            if (count($item) == 2) {
                $config[trim($item[0])] = trim($item[1]);
            }
        }
        if ($isLocal && (empty($config['host']) || empty($config['port']) || empty($config['name']) || empty($config['user']) || empty($config['passwd']))) {
            throw new Exception('数据库配置信息不完整');
        }
        return $config;
    }

    private function manageRequest(string $type)
    {
        switch ($type) {
            case 'get':
                $this->getRequest();
                break;
            default:
                $this->jsonReturn(['code' => 400, 'msg' => self::REQUEST_TYPE_ERROR]);
                break;
        }
    }

    private function jsonReturn(array $data)
    {
        header('Content-Type: application/json');
        exit(json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
    }

    private function getDirFiles(string $dirPath, array $excludes = [])
    {
        $files = [];
        $baseDirLength = strlen(realpath($dirPath)) + 1;
        if (is_dir($dirPath)) {
            $dirIterator = new RecursiveDirectoryIterator($dirPath, FilesystemIterator::SKIP_DOTS);
            $iterator = new RecursiveIteratorIterator($dirIterator, RecursiveIteratorIterator::SELF_FIRST);
            foreach ($iterator as $file) {
                $realPath = $file->getRealPath();
                if ($file->isFile() && !$this->isExcluded(str_replace('\\', '/', substr($realPath, $baseDirLength)), $excludes)) {
                    $files[] = [
                        'realPath' => $realPath,
                        'relativePath' => $_SERVER['HTTP_HOST'] . '/' . str_replace('\\', '/', substr($realPath, $baseDirLength))
                    ];
                }
            }
        }
        return $files;
    }

    private function isExcluded(string $path, array $excludes)
    {
        foreach ($excludes as $exclude) {
            if (stripos($path, $exclude) !== false) {
                return true;
            }
        }
        return false;
    }

    private function asyncRequest($params = [])
    {
        $path = $_SERVER['PHP_SELF'];
        $params = http_build_query($params);
        if (strpos($path, '?') !== false) {
            $path .= '&' . $params;
        } else {
            $path .= '?' . $params;
        }
        $fp = fsockopen($_SERVER['HTTP_HOST'], 80, $errno, $errstr, 5);
        if (!$fp) {
            $this->jsonReturn(['code' => 400, 'msg' => '请求失败，请稍后重试!']);
        } else {
            stream_set_blocking($fp,true);
            stream_set_timeout($fp,1);
            $out = "GET " . $path . " HTTP/1.1\r\n";
            $out .= "Host: " . $_SERVER['HTTP_HOST'] . "\r\n";
            $out .= "Content-type:application/x-www-form-urlencoded\r\n";
            $out .= "Connection: Close\r\n\r\n";
            fwrite($fp, $out);
            usleep(1000);
            fclose($fp);
        }
    }

    private function curlRequest($url, $method = 'GET', $data = [], $headers = [], $file = null) 
    {
        $ch = curl_init();
        curl_setopt($ch, CURLOPT_URL, $url);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($ch, CURLOPT_HEADER, false);
        if (!empty($headers)) {
            curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
        }
        switch (strtoupper($method)) {
            case 'POST':
                curl_setopt($ch, CURLOPT_POST, true);
                if ($file) {
                    if (is_array($data)) {
                        $data[$file['form_field']] = new CURLFile($file['path'], $file['mime'], $file['name']);
                    }
                }
                curl_setopt($ch, CURLOPT_POSTFIELDS, $data);
                break;
            case 'GET':
                if (!empty($data)) {
                    $url .= '?' . http_build_query($data);
                    curl_setopt($ch, CURLOPT_URL, $url);
                }
                break;
        }
        curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
        $response = curl_exec($ch);
        if (curl_errno($ch)) {
            $error_msg = curl_error($ch);
        }
        curl_close($ch);
        if (isset($error_msg)) {
            return $error_msg;
        }
        return $response;
    }

}

class DownloadService
{
    private $siteUrl = '';

    private $header = [];

    private $burstBytes = 1024;

    public function setUrl(string $url): DownloadService
    {
        $this->siteUrl = $url;
        return $this;
    }

    public function setBurst(int $byte): DownloadService
    {
        $this->burstBytes = $byte;
        return $this;
    }

    public function setHeader(array $header): DownloadService
    {
        $this->header = $header;
        return $this;
    }

    public function saveFile(string $fileName, array $dbConfig = [])
    {
        try{
            function setCacheContent($progress, $status, $result) {
                $data = json_encode([
                    'progress' => $progress,
                    'status' => $status,
                    'result' => $result
                ], JSON_UNESCAPED_UNICODE);
                file_put_contents(__DIR__ . '/.temp', $data);
            };
            $siteFileInfo = $this->getSiteFiLeInfo();
            $siteFileLength = $siteFileInfo['Content-Length'] ?? 0;
            if (file_exists($fileName)) {
                $fd = fopen($fileName, 'ab');
            } else {
                $fd = fopen($fileName, 'wb');
            }
            if (!$fd) {
                throw new Exception('创建或打开本地文件失败!');
            }
            if (!flock($fd, LOCK_EX | LOCK_NB)) {
                throw new Exception('已有相关进程操作执行下载本文件!');
            }
            $fileSize = filesize($fileName);
            if ($fileSize && $fileSize >= $siteFileLength) {
                throw new Exception('原文件已下载完成,请勿重复下载!');
            }
            $sByte = $fileSize;
            $eByte = $sByte + $this->burstBytes;
            $suffix = pathinfo($fileName, PATHINFO_EXTENSION);
            $progress = ($fileSize / $siteFileLength) * 100;
            setCacheContent($progress, 0, '下载中...');
            while (true) {
                if ($fileSize >= $siteFileLength) {
                    fclose($fd);
                    if (in_array($suffix, ['zip', 'tar', 'gz', 'rar'])) {
                        setCacheContent(100, 2, '正在解压中...');
                        $this->unzipFile($fileName);
                        setCacheContent(100, 2, '正在导入数据库...');
                        $this->importDatabase($fileName, $dbConfig);
                        if (file_exists($fileName)) {
                            unlink($fileName);
                        }
                    }
                    sleep(5);
                    unlink(__DIR__ . '/.temp');
                    setCacheContent(100, 1, '下载完成!');
                    break;
                }
                $xRange = "{$sByte}-{$eByte}";
                $result = $this->curl($xRange);
                $code = $result['code'] ?? 0;
                if (!$code) {
                    throw new Exception('Http请求异常!');
                }
                if ($code != 206 && $code != 200) {
                    throw new Exception('Http状态码异常,可能不支持断点的资源或已完成下载!');
                }
                $streamLength = $result['length'] ?? 0;
                $streamContent = $result['stream'] ?? '';
                
                if ($streamLength > 0) {
                    $saveRes = fwrite($fd, $streamContent);
                    if (!$saveRes) {
                        throw new Exception('写入流到文件失败!');
                    }
                    if ($saveRes != $streamLength) { 
                        throw new Exception('数据异常:返回大小和写入大小不一致!');
                    }
                    $fileSize += $saveRes;
                    // 读取当前文件大小，和$streamLength计算进度
                    $progress = ($fileSize / $siteFileLength) * 100;
                    setCacheContent($progress, 0, '下载中...');
                    $sByte = $eByte + 1;
                    $eByte = $sByte + $this->burstBytes;
                }
            }
        } catch (Exception $e) {
            sleep(5);
            unlink(__DIR__ . '/.temp');
            setCacheContent(0, -1, $e->getMessage());
        }
    }

    // 解压文件
    private function unzipFile(string $file)
    {
        $zip = new ZipArchive();
        if ($zip->open($file) === TRUE) {
            $zip->extractTo(dirname($file));
            $zip->close();
        }else{
            throw new Exception('文件解压失败!');
        }
    }

    // 导入数据库
    private function importDatabase(string $file, array $dbConfig)
    {
        if (empty($dbConfig)) {
            return;
        }
        try {
            $sqlFileData = [];
            $zip = new \ZipArchive();
            if ($zip->open($file) === true) {
                for ($i = 0; $i < $zip->numFiles; $i++) {
                    $name = $zip->getNameIndex($i);
                    if (strpos($name, '_tmd.sql') !== false) {
                        $sqlFileData[] = $name;
                    }
                }
                $zip->close();
            }else{
                throw new Exception('更新数据库失败，无法打开压缩包');
            }
            if (empty($sqlFileData)) {
                throw new Exception('更新数据库失败，未找到.sql文件');
            }
            $mysql = new Mysql([
                'db1' => [
                    'host' => $dbConfig['host'],
                    'port' => $dbConfig['port'],
                    'user' => $dbConfig['user'],
                    'passwd' => $dbConfig['passwd'],
                    'dbname' => $dbConfig['name'],
                    'prefix' => $dbConfig['prefix'],
                ],
            ]);
            foreach ($sqlFileData as $value) {
                $sqlFile = dirname($file) . '/' . $value;
                if (!file_exists($sqlFile)) {
                    continue;
                }
                $sqlContent = file_get_contents($sqlFile);
                $sqlContent = preg_replace("/(\/\*.*?\*\/|--.*?$)/ms", '', $sqlContent);
                $sqlContent = explode(";\r\n", $sqlContent);

                array_filter($sqlContent, function($value) { return $value !== ''; });
                foreach ($sqlContent as $sqlStatement) {
                    if (empty($sqlStatement)) {
                        continue;
                    }
                    $mysql->import('db1',$sqlStatement);
                }
            }
        } catch (Exception $e) {
            if (file_exists($file)) {
                unlink($file);
            }
            throw new Exception($e->getMessage());
        }
    }


    private function getSiteFiLeInfo(): array
    {
        if (!$this->siteUrl) {
            throw new Exception('请先设置远程文件url!');
        }
        $responseHeader = get_headers($this->siteUrl, 1);
        if (!$responseHeader) {
            throw new Exception('获取远程文件信息失败!');
        }
        if (!empty($responseHeader['Location'])) {

            $this->siteUrl = $responseHeader['Location'];
            return $this->getSiteFiLeInfo();
        }

        return $responseHeader;
    }

    private function curl(string $range): array
    {
        $ch = curl_init();
        curl_setopt($ch, CURLOPT_URL, $this->siteUrl);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
        curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'GET');
        curl_setopt($ch, CURLOPT_HEADER, TRUE);

        curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0);
        curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0);

        curl_setopt($ch, CURLOPT_RANGE, $range);

        if ($this->header) {
            curl_setopt($ch, CURLOPT_HTTPHEADER, $this->header);
        }

        $response = curl_exec($ch);
        if (curl_errno($ch)) {
            throw new Exception('下载文件异常:' . curl_error($ch));
        }

        $headSize = curl_getinfo($ch, CURLINFO_HEADER_SIZE);
        $httpHeader = substr($response, 0, $headSize);
        if (!$httpHeader) {
            throw new Exception('下载文件异常:未获取到响应头');
        }
        $fileStream = substr($response, $headSize);

        $length = $this->getResHeaderValue('Content-Length', $httpHeader);
        $httpCode = $this->getResHeaderValue('Http-Code', $httpHeader);
        curl_close($ch);

        return [
            'code' => $httpCode,
            'length' => $length,
            'stream' => $fileStream,
        ];
    }

    private function getResHeaderValue(string $key, string $responseHead): string
    {
        $value = '';
        $headArr = explode("\r\n", $responseHead);
        foreach ($headArr as $loop) {
            if ($key == 'Http-Code') {
                if (preg_match('/HTTP\/[1-3](\.[0-9])? ([0-9]{3})/', $loop, $matches)) {
                    return $matches[2];
                }
            } else {
                $lowerLoop = strtolower($loop);
                $lowerKey = strtolower($key);
                if (strpos($lowerLoop, $lowerKey) !== false) {
                    $value = trim(str_replace($lowerKey . ':', '', $lowerLoop));
                }
            }
        }
        return $value;
    }
}

class Mysql
{
    private $prefix;
    private $connections = [];

    public function __construct($config)
    {
        foreach ($config as $name => $params) {
            $this->addConnection($name, $params);
        }
    }

    private function addConnection($name, $params)
    {
        $dbname = $params['dbname'];
        $this->prefix[$name] = $params['prefix'];
        try {
            if (!empty($params['autoCreateDb'])) {
                $pdo = new PDO("mysql:host={$params['host']};port={$params['port']}", $params['user'], $params['passwd']);
                $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
                $result = $pdo->query("SELECT COUNT(*) FROM INFORMATION_SCHEMA.SCHEMATA WHERE SCHEMA_NAME = '$dbname'");
                if ($result->fetchColumn() == 0) {
                    $pdo->exec("CREATE DATABASE `$dbname` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci");
                }
            }
            $this->connections[$name] = new PDO(
                "mysql:host={$params['host']};port={$params['port']};dbname={$dbname}",
                $params['user'],
                $params['passwd']
            );
            $this->connections[$name]->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
            $this->connections[$name]->exec('set names utf8');
            if (!empty($params['prefix'])) {
                if (isset($params['connectError']) && $params['connectError'] == false) {
                    return;
                }
                if (!$this->validatePrefix($name, $params['prefix'])) {
                    throw new Exception("数据库前缀验证失败");
                }
            }
        } catch (Exception $e) {
            if (isset($params['connectError']) && $params['connectError'] == false) {
                return;
            }
            throw new Exception("数据库连接失败 : " . $e->getMessage());
        }
    }

    public function query($name, $sql)  
    {
        $conn = $this->connections[$name] ?? null;
        if (!$conn) {
            throw new Exception("未找到名为 {$name} 的数据库连接");
        }

        $result = $conn->query($sql);
        if (!$result) {
            throw new Exception('数据库操作失败: ' . $conn->errorInfo()[2]);
        }
        return $result;
    }

    public function import($name, $sql)
    {
        $conn = $this->connections[$name] ?? null;
        if (!$conn) {
            throw new Exception("未找到名为 {$name} 的数据库连接");
        }

        $conn->beginTransaction();
        try {
            $sql = str_replace('tmd_', $this->prefix[$name], $sql);
            $sqlArr = explode(';', $sql);
            foreach ($sqlArr as $sql) {
                $sql = trim($sql);
                if ($sql) {
                    $this->query($name, $sql);
                }
            }
            $conn->commit();
        } catch (Exception $e) {
            $conn->rollback();
            throw new Exception('数据库导入失败: ' . $e->getMessage());
        }
    }

    private function validatePrefix($name, $expectedPrefix)
    {
        $conn = $this->connections[$name];
        $sql = "SHOW TABLES";
        $statement = $conn->query($sql);
        $tables = $statement->fetchAll(PDO::FETCH_COLUMN);

        $verifyResult = false;
        foreach ($tables as $table) {
            if (strpos($table, $expectedPrefix) === 0) {
                $verifyResult = true;
                break;
            }
        }
        return $verifyResult;
    }

    public function migrateTable($sourceName, $targetName, $tableName)
    {
        $sourceConn = $this->connections[$sourceName];
        $targetConn = $this->connections[$targetName] ?? null;
        // 如果targetConn无法连接，则不迁移数据
        if (!$targetConn) {
            return;
        }

        // 检查并创建表结构
        $structure = $sourceConn->query("SHOW CREATE TABLE ".$this->prefix[$sourceName].$tableName)->fetch();
        $createTableSql = $structure['Create Table'];

        // 替换表前缀
        if (empty($this->prefix[$sourceName])){
            $this->prefix[$targetName] = '';
        } else {
            $createTableSql = str_replace($this->prefix[$sourceName], $this->prefix[$targetName], $createTableSql);
        }

        // 在目标数据库中创建表（如果已存在，则先删除）
        $targetConn->exec("DROP TABLE IF EXISTS ".$this->prefix[$targetName].$tableName);
        $targetConn->exec($createTableSql);

        // 获取源数据库表的数据
        $data = $sourceConn->query("SELECT * FROM ".$this->prefix[$sourceName].$tableName)->fetchAll(PDO::FETCH_ASSOC);

        // 插入数据到目标数据库
        $targetConn->beginTransaction();
        try {
            foreach ($data as $row) {
                $columns = implode(", ", array_keys($row));
                $values = implode(", ", array_map(function ($value) use ($targetConn) {
                    return $targetConn->quote($value);
                }, array_values($row)));
                $targetConn->exec("INSERT INTO ".$this->prefix[$targetName].$tableName." ({$columns}) VALUES ({$values})");
            }
            $targetConn->commit();
        } catch (Exception $e) {
            $targetConn->rollback();
        }
    }
}
$tmd = new Tmd();
?>
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Tmd安装工具</title>
    <link href="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-5-y/element-ui/2.15.4/theme-chalk/index.min.css" type="text/css" rel="stylesheet" />
    <style>
        body { margin: 0; padding: 0; font-family: Arial, sans-serif; background-color: #f0f2f5; height: 100vh;}
        h1, h2, h3, h4, h5, h6 { margin: 0; padding: 0; }
        :root {
            --designate-width: 450px;
        }
        #app{
            width: 100%;
            height: 100%;
        }
        .el-header{
            position: absolute;
            background-color: #409EFF;
            color: #fff;
            width: 100%;
            max-width: var(--designate-width);
            display: flex;
            justify-content: space-between;
            align-items: center;
        }
        .el-header .right{
            display: flex;
            align-items: center;
        }
        .el-header .right a + a{
            margin-left: 25px;
        }
        .el-main {
            padding-top: 70px !important;
            box-shadow: 0 0 10px rgba(0, 0, 0, 0.3);
        }
        .el-container{
            height: 100%;
            flex-direction: row-reverse;
        }
        .detb-content{
            position: absolute;
            width: 467px;
            background-color: #fff;
            padding: 20px;
            box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
            border-radius: 5px;
            bottom: 100px;
        }
        .loading{
            position: fixed;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            background-color: #f2f2f2;
            display: flex;
            justify-content: center;
            align-items: center;
            flex-direction: column;
            z-index: 999;
            color: #fff;
            font-size: 20px;
        }
        .loading .loading-icons{
            width: 40px;
            height: 40px;
            border-radius: 50%;
            border: 5px solid #fff;
            border-top-color: #409EFF;
            animation: loading 1s linear infinite;
        }
        @keyframes loading{
            0%{
                transform: rotate(0deg);
            }
            100%{
                transform: rotate(360deg);
            }
        }
        .loading .title{
            color: #000;
            margin-top: 25px;
            font-weight: 600;
        }

        @media screen and (max-width: 1000px) {
            :root {
                --designate-width: 100%;
            }
            .el-container .el-container{
                display: block;
                margin: 15px;
            }
            .el-container section{
                padding-top: 60px;
            }
            .el-container section aside {
                display: none !important;
            }
            .el-main {
                padding-top: 15px !important;
                box-shadow: none;
            }
        }
    </style>
    <script>
        var backdropImg = '<?php echo $tmd->getPageData('backdropImg') ?? ''; ?>';
        var debtData = <?php echo json_encode($tmd->getPageData('debtData') ?? [], JSON_UNESCAPED_UNICODE); ?>;
    </script>
</head>
<body>
    <div id="app">
        <el-container>
            <el-header>
                <h4>Tmd下载工具1.0</h4>
                <div class="right">
                    <a href="https://gitee.com/vi2_admin/tmd-dow-tool" target="_blank" class="gitee">
                        <svg t="1720816173176" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1443" width="24" height="24">
                            <path d="M512 1021.72444445A509.72444445 509.72444445 0 1 1 512 2.27555555a509.72444445 509.72444445 0 0 1 0 1019.4488889z m257.99338667-566.37667556H480.54272a25.19495111 25.19495111 0 0 0-25.19495111 25.19495111v62.91456c0 13.90819555 11.28675555 25.19495111 25.12213333 25.19495111h176.21902223c13.98101333 0 25.19495111 11.28675555 25.1949511 25.12213334v12.59747555c0 41.72458667-33.78744889 75.51203555-75.51203555 75.51203555H367.23825778a25.19495111 25.19495111 0 0 1-25.12213333-25.12213333V417.62816c0-41.72458667 33.78744889-75.51203555 75.43921777-75.51203555h352.43804445c13.83537778 0 25.12213333-11.28675555 25.12213333-25.19495112v-62.91456a25.19495111 25.19495111 0 0 0-25.12213333-25.19495111h-352.43804445a188.74368 188.74368 0 0 0-188.74368 188.81649778v352.36522667c0 13.90819555 11.28675555 25.19495111 25.19495111 25.19495111h371.22503112a169.88387555 169.88387555 0 0 0 169.95669333-169.88387556V480.54272a25.19495111 25.19495111 0 0 0-25.19495111-25.19495111z" fill="#C71D23" p-id="1444"></path>
                        </svg>
                    </a>
                    <a href="https://github.com/1532332928/tmd-dow-tool" target="_blank" class="github">
                        <svg t="1720816379864" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="11545" width="24" height="24">
                            <path d="M512 12.64c-282.752 0-512 229.216-512 512 0 226.208 146.72 418.144 350.144 485.824 25.6 4.736 35.008-11.104 35.008-24.64 0-12.192-0.48-52.544-0.704-95.328-142.464 30.976-172.512-60.416-172.512-60.416-23.296-59.168-56.832-74.912-56.832-74.912-46.464-31.776 3.52-31.136 3.52-31.136 51.392 3.616 78.464 52.768 78.464 52.768 45.664 78.272 119.776 55.648 148.992 42.56 4.576-33.088 17.856-55.68 32.512-68.48-113.728-12.928-233.28-56.864-233.28-253.024 0-55.904 20-101.568 52.768-137.44-5.312-12.896-22.848-64.96 4.96-135.488 0 0 43.008-13.76 140.832 52.48 40.832-11.36 84.64-17.024 128.16-17.248 43.488 0.192 87.328 5.888 128.256 17.248 97.728-66.24 140.64-52.48 140.64-52.48 27.872 70.528 10.336 122.592 5.024 135.488 32.832 35.84 52.704 81.536 52.704 137.44 0 196.64-119.776 239.936-233.792 252.64 18.368 15.904 34.72 47.04 34.72 94.816 0 68.512-0.608 123.648-0.608 140.512 0 13.632 9.216 29.6 35.168 24.576 203.328-67.776 349.856-259.616 349.856-485.76 0-282.784-229.248-512-512-512z" fill="#000000" p-id="11546"></path>
                        </svg>
                    </a>
                </div>
            </el-header>
            <el-container>
                <el-aside :style="asideStyle">
                    <img :src="backdropImg" alt="backdrop" style="width: 800px;">
                    <div class="detb-content" v-if="debtData">
                        <el-descriptions class="margin-top" title="债务数据" :column="3" size="mini" border>
                            <template slot="extra">
                                <el-link type="primary" href="https://afdian.net/a/cntmd?tab=home" target="_blank">为我发电</el-link>
                            </template>
                            <el-descriptions-item v-for="item in debtData">
                                <template slot="label">
                                {{ item.name }}
                                </template>
                                {{ item.amount }}
                            </el-descriptions-item>
                        </el-descriptions>
                        <p style="color: red;">一身债务要打多久螺丝才能还清？</p>
                        <p style="margin-bottom: 0;color: red;">盗卖只会让我的困境持续难以翻身,别让我在去打螺丝了,好嘛?🙂</p>
                    </div>
                </el-aside>
                <el-main :style="mainStyle">
                    <el-alert
                    :title="alertShow.title"
                    :type="alertShow.type"
                    :description="alertShow.description"
                    show-icon
                    v-if="alertShow"
                    >
                    </el-alert>
                    <el-form :model="form" ref="form" label-position="top" status-icon :rules="rules">
                        <el-form-item label="识别编码" prop="identifyCode" :required="true">
                            <el-input 
                            type="textarea" 
                            v-model="form.identifyCode" 
                            placeholder="根据作者提供的对应识别码下载对应的产品"
                            :disabled="product != null ? true : false"
                        ></el-input>
                        </el-form-item>
                        <el-form-item label="产品版本" prop="versionId" :required="true" v-if="product">
                            <el-select 
                            style="width: 100%;" 
                            v-model="form.versionId" 
                            placeholder="请选择需要下载的版本"
                            @change="handleVersionChange"
                            :disabled="loading.download"
                            >
                                <el-option 
                                v-for="item in product.product_data" 
                                :label="`${product.name} - (${item.version_sign} ${item.version})`"
                                :value="item.id"
                                >
                                    <div style="margin-top: -5px;">{{product.name}} - ({{ item.version_sign }} {{ item.version }})</div>
                                    <div style="color: #8492a6; font-size: 11px;margin-top: -18px;">{{ item.updated_at }}</div>
                                </el-option>
                            </el-select>
                        </el-form-item>
                        <el-alert 
                        v-if="details"
                        :title="details"
                        type="info"
                        :closable="false"
                        style="margin-bottom: 15px;"
                        >
                        </el-alert>
                        <el-form-item label="下载目录" prop="downloadDir" :required="true" v-if="product">
                            <el-input 
                            v-model="form.downloadDir" 
                            placeholder="一定是/开头的，如：/admin/"
                            :disabled="loading.download"
                            ></el-input>
                        </el-form-item>
                        <el-form-item label="数据库配置" prop="databaseConfig" :required="true" v-if="database_up">
                            <el-input 
                            type="textarea" 
                            v-model="form.databaseConfig" 
                            placeholder="当前项目需要导入数据库"
                            :disabled="loading.download"
                            rows="5"
                            ></el-input>
                            <p style="margin: 0;font-size: 11px;color: #f56c6c;">注意：一行一个配置，请勿把：删除</p>
                        </el-form-item>
                        <el-form-item>
                            <el-button 
                            type="primary" 
                            @click="getProductData()" 
                            v-if="product==null"
                            :loading="loading.getProduct"
                            >查询</el-button>
                            <el-button 
                            type="primary" 
                            @click="execDownload()" 
                            v-if="product"
                            :loading="loading.download"
                            >{{ downloadTitle }}</el-button>
                        </el-form-item>
                    </el-form>
                </el-main>
            </el-container>
        </el-container>
        <div class="loading" v-if="loadingState">
            <div class="loading-icons"></div>
            <div class="title">正在渲染页面...</div>
        </div>
    </div>
    <script src="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-5-y/vue/2.6.14/vue.min.js"></script>
    <script src="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-5-y/element-ui/2.15.4/index.min.js"></script>
    <script src="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-5-y/axios/0.26.0/axios.min.js"></script>
    <script>
        var app = new Vue({
            el: '#app',
            data: {
                loadingState: true,
                asideStyle: {
                    flex: 1,
                    width: 'auto',
                    display: 'flex',
                    justifyContent: 'center',
                    alignItems: 'center',
                    position: 'static',
                },
                mainStyle: {
                    flex: 2,
                    maxWidth: 'var(--designate-width)',
                    padding: '20px',
                    backgroundColor: '#fff',
                    overflowY: 'auto'
                },
                backdropImg: backdropImg,
                debtData: debtData,
                database_up: false,
                form: {
                    identifyCode: '',
                    versionId: '',
                    downloadDir: '',
                    databaseConfig: 'host:localhost\r\nport:3306\r\nname:数据库名\r\nuser:用户名\r\npasswd:密码\r\nprefix:前缀'
                },
                rules: {
                    identifyCode: [
                        { validator: (rule, value, callback) => {
                            if (!value) {
                                callback(new Error('请输入识别编码'));
                            } else {
                                callback();
                            }
                        }, trigger: 'blur' }
                    ],
                    versionId: [
                        { validator: (rule, value, callback) => {
                            if (!value) {
                                callback(new Error('请选择下载版本'));
                            } else {
                                callback();
                            }
                        }, trigger: 'blur' }
                    ],
                    downloadDir: [
                        { validator: (rule, value, callback) => {
                            if (!value) {
                                callback(new Error('请设置下载目录'));
                            } else if (value.indexOf('/') !== 0) {
                                callback(new Error('下载目录必须以/开头'));
                            } else {
                                callback();
                            }
                        }, trigger: 'blur' }
                    ]
                },
                loading: {
                    getProduct: false,
                    download: false
                },
                details: '',
                product: null,
                downloadTitle: '开始下载',
                alertShow: null,
            },
            methods: {
                handleVersionChange(value) {
                    const selectedVersion = this.product.product_data.find(item => item.id === value);
                    if (selectedVersion) {
                        this.details = selectedVersion.description;
                        this.database_up = selectedVersion.database_up;
                    }
                },
                getProductData() {
                    this.$refs['form'].validate((valid) => {
                        if (!valid) {
                            return false;
                        }else{
                            this.alertShow = null;
                            this.loading.getProduct = true;
                            axios.get('', {
                                params: {
                                    page: 'getProduct',
                                    identifyCode: this.form.identifyCode
                                }
                            }).then(function (response) {
                                app.loading.getProduct = false;
                                if (response.data.code === 200) {
                                    app.product = response.data.data;
                                } else {
                                    app.$message.error(response.data.msg);
                                }
                            }).catch(function (error) {
                                app.loading.getProduct = false;
                                app.$message.error('请求失败，请稍后重试!');
                            });
                        }
                    });
                },
                execDownload() {
                    this.$refs['form'].validate((valid) => {
                        if (!valid) {
                            return false;
                        }else{
                            this.alertShow = null;
                            this.loading.download = true;
                            axios.get('', {
                                params: {
                                    page: 'download',
                                    versionId: this.form.versionId,
                                    downloadDir: this.form.downloadDir,
                                    identifyCode: this.form.identifyCode,
                                    databaseConfig: this.form.databaseConfig
                                }
                            }).then(function (response) {
                                if (response.data.code === 200) {
                                    const timer = setInterval(() => {
                                        axios.get('?page=getResults').then(function (response) {
                                            const progress = response.data.data;
                                            app.downloadTitle = (()=> {
                                                if (progress.status == 0) {
                                                    return '已下载：' + progress.progress.toFixed(2) + '%';
                                                }else if(progress.status == 1){
                                                    app.alertShow = {
                                                        title: '下载完成',
                                                        type: 'success',
                                                        description: '文件已保存到：' + app.form.downloadDir
                                                    };
                                                    app.loading.download = false;
                                                    app.product = null;
                                                    app.form = {
                                                        identifyCode: '',
                                                        versionId: '',
                                                        downloadDir: ''
                                                    };
                                                    app.database_up = false;
                                                    clearInterval(timer);
                                                    return '开始下载';
                                                }else if(progress.status == 2){
                                                    return progress.result;
                                                }else{
                                                    app.loading.download = false;
                                                    clearInterval(timer);
                                                    app.alertShow = {
                                                        title: '下载失败',
                                                        type: 'error',
                                                        description: progress.result
                                                    };
                                                    return '开始下载';
                                                }
                                            })();
                                        }).catch(function (error) {
                                            app.loading.download = false;
                                            clearInterval(timer);
                                            app.$message.error('请求失败，请稍后重试!');
                                        });
                                    }, 1000);
                                } else {
                                    app.loading.download = false;
                                    app.$message.error(response.data.msg);
                                }
                            }).catch(function (error) {
                                app.loading.download = false;
                                app.$message.error('请求失败，请稍后重试!');
                            });
                        }
                    }); 
                }
            },
            mounted: function () {
                this.$nextTick(function () {
                    setTimeout(() => {
                        this.loadingState = false;
                    }, 1000);
                })
            }
        });
    </script>
</body>
</html>